/*
 * Copyright 2010 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.android.apps.authenticator;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Locale;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import android.content.ContentValues;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.os.Process;
import android.provider.Settings.Secure;
import android.util.Log;

import com.google.android.apps.authenticator.Base32String.DecodingException;
import com.google.android.apps.authenticator.PasscodeGenerator.Signer;

/**
 * A database of email addresses and secret values
 *
 * @author sweis@google.com (Steve Weis)
 */
public class AccountDb {
  public static final Integer DEFAULT_HOTP_COUNTER = 0;

  public static final String GOOGLE_CORP_ACCOUNT_NAME = "Google Internal 2Factor";

  private static final String ID_COLUMN = "_id";
  private static final String EMAIL_COLUMN = "email";
  private static final String SECRET_COLUMN = "secret";
  private static final String COUNTER_COLUMN = "counter";
  private static final String TYPE_COLUMN = "type";
  
  private static final String KEY_COLUMN = "key";
  private static final String VALUE_COLUMN = "value";
  
  // @VisibleForTesting
  static final String PROVIDER_COLUMN = "provider";
  // @VisibleForTesting
  static final String TABLE_NAME = "accounts";

  // @VisibleForTesting
  static final String PATH = "databases";

  private static final String TABLE_INFO_COLUMN_NAME_COLUMN = "name";

  private static final int PROVIDER_UNKNOWN = 0;
  private static final int PROVIDER_GOOGLE = 1;
  private static final int HASH_INTERATIONS=5;

  //Configuration Table
  static final String TABLE2_NAME = "config";
  // Configurations
  private static final String CONFIG_ENCRYPTED="encrypted";
  private static final String CONFIG_ENCRYPTED_TRUE="true";
  private static final String CONFIG_ENCRYPTED_FALSE="false";
  private static final String CONFIG_PASSWORD_HASH = "PasswordHash";
  
  // @VisibleForTesting
  SQLiteDatabase mDatabase;

  private static final String LOCAL_TAG = "GoogleAuthenticator.AccountDb";


  
  private Context myContext;
  private byte[] passwordHash=null;

  /**
   * Types of secret keys.
   */
  public enum OtpType {  // must be the same as in res/values/strings.xml:type
    TOTP (0),  // time based
    HOTP (1);  // counter based

    public final Integer value;  // value as stored in SQLite database
    OtpType(Integer value) {
      this.value = value;
    }

    public static OtpType getEnum(Integer i) {
      for (OtpType type : OtpType.values()) {
        if (type.value.equals(i)) {
          return type;
        }
      }

      return null;
    }

  }

  public AccountDb(Context context) {
	myContext=context;
    mDatabase = openDatabase(context);

    // Create the table if it doesn't exist
    mDatabase.execSQL(String.format(
        "CREATE TABLE IF NOT EXISTS %s" +
        " (%s INTEGER PRIMARY KEY, %s TEXT NOT NULL, %s TEXT NOT NULL, " +
        " %s INTEGER DEFAULT %s, %s INTEGER, %s INTEGER DEFAULT %s)",
        TABLE_NAME, ID_COLUMN, EMAIL_COLUMN, SECRET_COLUMN, COUNTER_COLUMN,
        DEFAULT_HOTP_COUNTER, TYPE_COLUMN,
        PROVIDER_COLUMN, PROVIDER_UNKNOWN));

    // Create the config table if it doesn't exist
    mDatabase.execSQL(String.format(
        "CREATE TABLE IF NOT EXISTS %s" +
        " (%s TEXT NOT NULL, %s TEXT NOT NULL) ",
        TABLE2_NAME, KEY_COLUMN, VALUE_COLUMN));

    Collection<String> tableColumnNames = listTableColumnNamesLowerCase();
    if (!tableColumnNames.contains(PROVIDER_COLUMN.toLowerCase(Locale.US))) {
      // Migrate from old schema where the PROVIDER_COLUMN wasn't there
      mDatabase.execSQL(String.format(
          "ALTER TABLE %s ADD COLUMN %s INTEGER DEFAULT %s",
          TABLE_NAME, PROVIDER_COLUMN, PROVIDER_UNKNOWN));
    }
    
  }

/*
   * Tries three times to open database before throwing AccountDbOpenException.
   */
  private SQLiteDatabase openDatabase(Context context) {
    for (int count = 0; true; count++) {
      try {
        return context.openOrCreateDatabase(PATH, Context.MODE_PRIVATE, null);
      } catch (SQLiteException e) {
        if (count < 2) {
          continue;
        } else {
          throw new AccountDbOpenException("Failed to open AccountDb database in three tries.\n"
              + getAccountDbOpenFailedErrorString(context), e);
        }
      }
    }
  }

  private String getAccountDbOpenFailedErrorString(Context context) {
    String dataPackageDir = context.getApplicationInfo().dataDir;
    String databaseDirPathname = context.getDatabasePath(PATH).getParent();
    String databasePathname = context.getDatabasePath(PATH).getAbsolutePath();
    String[] dirsToStat = new String[] {dataPackageDir, databaseDirPathname, databasePathname};
    StringBuilder error = new StringBuilder();
    int myUid = Process.myUid();
    for (String directory : dirsToStat) {
      try {
        FileUtilities.StatStruct stat = FileUtilities.getStat(directory);
        String ownerUidName = null;
        try {
          if (stat.uid == 0) {
            ownerUidName = "root";
          } else {
            PackageManager packageManager = context.getPackageManager();
            ownerUidName = (packageManager != null) ? packageManager.getNameForUid(stat.uid) : null;
          }
        } catch (Exception e) {
          ownerUidName = e.toString();
        }
        error.append(directory + " directory stat (my UID: " + myUid);
        if (ownerUidName == null) {
          error.append("): ");
        } else {
          error.append(", dir owner UID name: " + ownerUidName + "): ");
        }
        error.append(stat.toString() + "\n");
      } catch (IOException e) {
        error.append(directory + " directory stat threw an exception: " + e + "\n");
      }
    }
    return error.toString();
  }

  /**
   * Closes this database and releases any system resources held.
   */
  public void close() {
    mDatabase.close();
  }

  /**
   * Lists the names of all the columns in the specified table.
   */
  // @VisibleForTesting
  static Collection<String> listTableColumnNamesLowerCase(
      SQLiteDatabase database, String tableName) {
    Cursor cursor =
        database.rawQuery(String.format("PRAGMA table_info(%s)", tableName), new String[0]);
    Collection<String> result = new ArrayList<String>();
    try {
      if (cursor != null) {
        int nameColumnIndex = cursor.getColumnIndexOrThrow(TABLE_INFO_COLUMN_NAME_COLUMN);
        while (cursor.moveToNext()) {
          result.add(cursor.getString(nameColumnIndex).toLowerCase(Locale.US));
        }
      }
      return result;
    } finally {
      tryCloseCursor(cursor);
    }
  }

  /**
   * Lists the names of all the columns in the accounts table.
   */
  private Collection<String> listTableColumnNamesLowerCase() {
    return listTableColumnNamesLowerCase(mDatabase, TABLE_NAME);
  }

  /*
   * deleteAllData() will remove all rows. Useful for testing.
   */
  public boolean deleteAllData() {
    mDatabase.delete(AccountDb.TABLE_NAME, null, null);
    setConfig(CONFIG_ENCRYPTED, CONFIG_ENCRYPTED_FALSE);
    resetPassword();
    deletePasswordHash();
    return true;
  }

  public boolean nameExists(String email) {
    Cursor cursor = getAccount(email);
    try {
      return !cursorIsEmpty(cursor);
    } finally {
      tryCloseCursor(cursor);
    }
  }

  public String getSecret(String email, boolean isEncrypted) {
    Cursor cursor = getAccount(email);
    try {
      if (!cursorIsEmpty(cursor)) {
        cursor.moveToFirst();
        if (isEncrypted)
          return decryptString(cursor.getString(cursor.getColumnIndex(SECRET_COLUMN)));
        else
          return cursor.getString(cursor.getColumnIndex(SECRET_COLUMN));
      }
    } finally {
      tryCloseCursor(cursor);
    }
    return null;
  }
  
  public String getSecret(String email) {
    return getSecret(email, isEncrypted());
  }

  static Signer getSigningOracle(String secret) {
    try {
      byte[] keyBytes = decodeKey(secret);
      final Mac mac = Mac.getInstance("HMACSHA1");
      mac.init(new SecretKeySpec(keyBytes, ""));

      // Create a signer object out of the standard Java MAC implementation.
      return new Signer() {
        @Override
        public byte[] sign(byte[] data) {
          return mac.doFinal(data);
        }
      };
    } catch (DecodingException error) {
      Log.e(LOCAL_TAG, error.getMessage());
    } catch (NoSuchAlgorithmException error) {
      Log.e(LOCAL_TAG, error.getMessage());
    } catch (InvalidKeyException error) {
      Log.e(LOCAL_TAG, error.getMessage());
    }

    return null;
  }

  private static byte[] decodeKey(String secret) throws DecodingException {
	  return Base32String.decode(secret);
  }

  private static String encodeKey(byte[] secret) throws DecodingException {
	  return Base32String.encode(secret);
  }

  public Integer getCounter(String email) {
    Cursor cursor = getAccount(email);
    try {
      if (!cursorIsEmpty(cursor)) {
        cursor.moveToFirst();
        return cursor.getInt(cursor.getColumnIndex(COUNTER_COLUMN));
      }
    } finally {
      tryCloseCursor(cursor);
    }
    return null;
  }

  void incrementCounter(String email) {
    ContentValues values = new ContentValues();
    values.put(EMAIL_COLUMN, email);
    Integer counter = getCounter(email);
    values.put(COUNTER_COLUMN, counter + 1);
    mDatabase.update(TABLE_NAME, values, whereClause(email), null);
  }

  public OtpType getType(String email) {
    Cursor cursor = getAccount(email);
    try {
      if (!cursorIsEmpty(cursor)) {
        cursor.moveToFirst();
        Integer value = cursor.getInt(cursor.getColumnIndex(TYPE_COLUMN));
        return OtpType.getEnum(value);
      }
    } finally {
      tryCloseCursor(cursor);
    }
    return null;
  }

  void setType(String email, OtpType type) {
    ContentValues values = new ContentValues();
    values.put(EMAIL_COLUMN, email);
    values.put(TYPE_COLUMN, type.value);
    mDatabase.update(TABLE_NAME, values, whereClause(email), null);
  }

  public boolean isGoogleAccount(String email) {
    Cursor cursor = getAccount(email);
    try {
      if (!cursorIsEmpty(cursor)) {
        cursor.moveToFirst();
        if (cursor.getInt(cursor.getColumnIndex(PROVIDER_COLUMN)) == PROVIDER_GOOGLE) {
          // The account is marked as source: Google
          return true;
        }
        // The account is from an unknown source. Could be a Google account added by scanning
        // a QR code or by manually entering a key
        String emailLowerCase = email.toLowerCase(Locale.US);
        return(emailLowerCase.endsWith("@gmail.com"))
            || (emailLowerCase.endsWith("@google.com"))
            || (email.equals(GOOGLE_CORP_ACCOUNT_NAME));
      }
    } finally {
      tryCloseCursor(cursor);
    }
    return false;
  }

  /**
   * Finds the Google corp account in this database.
   *
   * @return the name of the account if it is present or {@code null} if the account does not exist.
   */
  public String findGoogleCorpAccount() {
    return nameExists(GOOGLE_CORP_ACCOUNT_NAME) ? GOOGLE_CORP_ACCOUNT_NAME : null;
  }

  private static String whereClause(String email) {
    return EMAIL_COLUMN + " = " + DatabaseUtils.sqlEscapeString(email);
  }

  public void delete(String email) {
    mDatabase.delete(TABLE_NAME, whereClause(email), null);
  }

  /**
   * Save key to database, creating a new user entry if necessary.
   * @param email the user email address. When editing, the new user email.
   * @param secret the secret key.
   * @param oldEmail If editing, the original user email, otherwise null.
   * @param type hotp vs totp
   * @param counter only important for the hotp type
   */
  public void update(String email, String secret, String oldEmail,
      OtpType type, Integer counter) {
    update(email, secret, oldEmail, type, counter, null);
  }

  /**
   * Save key to database, creating a new user entry if necessary.
   * @param email the user email address. When editing, the new user email.
   * @param secret the secret key.
   * @param oldEmail If editing, the original user email, otherwise null.
   * @param type hotp vs totp
   * @param counter only important for the hotp type
   * @param googleAccount whether the key is for a Google account or {@code null} to preserve
   *        the previous value (or use a default if adding a key).
   */
  public void update(String email, String secret, String oldEmail,
      OtpType type, Integer counter, Boolean googleAccount) {
    ContentValues values = new ContentValues();
    values.put(EMAIL_COLUMN, email);
    if (isEncrypted())
      values.put(SECRET_COLUMN, encryptString(secret));
    else
      values.put(SECRET_COLUMN, secret);
    values.put(TYPE_COLUMN, type.ordinal());
    values.put(COUNTER_COLUMN, counter);
    if (googleAccount != null) {
      values.put(
          PROVIDER_COLUMN,
          (googleAccount.booleanValue()) ? PROVIDER_GOOGLE : PROVIDER_UNKNOWN);
    }
    int updated = mDatabase.update(TABLE_NAME, values,
                                  whereClause(oldEmail), null);
    if (updated == 0) {
      mDatabase.insert(TABLE_NAME, null, values);
    }
  }

  private Cursor getNames() {
    return mDatabase.query(TABLE_NAME, null, null, null, null, null, null, null);
  }

  private Cursor getAccount(String email) {
    return mDatabase.query(TABLE_NAME, null, EMAIL_COLUMN + "= ?",
        new String[] {email}, null, null, null);
  }

  /**
   * Returns true if the cursor is null, or contains no rows.
   */
  private static boolean cursorIsEmpty(Cursor c) {
    return c == null || c.getCount() == 0;
  }

  /**
   * Closes the cursor if it is not null and not closed.
   */
  private static void tryCloseCursor(Cursor c) {
    if (c != null && !c.isClosed()) {
      c.close();
    }
  }

  /**
   * Get list of all account names.
   * @param result Collection of strings-- account names are appended, without
   *               clearing this collection on entry.
   * @return Number of accounts added to the output parameter.
   */
  public int getNames(Collection<String> result) {
    Cursor cursor = getNames();

    try {
      if (cursorIsEmpty(cursor))
        return 0;

      int nameCount = cursor.getCount();
      int index = cursor.getColumnIndex(AccountDb.EMAIL_COLUMN);

      for (int i = 0; i < nameCount; ++i) {
        cursor.moveToPosition(i);
        String username = cursor.getString(index);
        result.add(username);
      }

      return nameCount;
    } finally {
      tryCloseCursor(cursor);
    }
  }

  private static class AccountDbOpenException extends RuntimeException {
    public AccountDbOpenException(String message, Exception e) {
      super(message, e);
    }
  }
  /**
   * Finds the key value in configuration database.
   *
   * @return cursor to the key.
   */
  private String getConfig(String key) {
    Cursor cursor=mDatabase.query(AccountDb.TABLE2_NAME, null, AccountDb.KEY_COLUMN + "= ?",
        new String[] {key}, null, null, null);
    
    try {
      if (cursorIsEmpty(cursor))
        return "";
      cursor.moveToFirst();
      int index = cursor.getColumnIndex(AccountDb.VALUE_COLUMN);
      return cursor.getString(index);
      } finally {
        tryCloseCursor(cursor);
      }
  }
  
  /**
   * Returns if database is encrypted.
   * Since no password is stored, this method relies on a configuration parameter. 
   *
   */
  public boolean isEncrypted() {
    if (getConfig(AccountDb.CONFIG_ENCRYPTED).equalsIgnoreCase(CONFIG_ENCRYPTED_TRUE))
      return true;
    else
      return false;
  }
  
  /**
   * Sets a value to the given key in configuration database.
   *
   * 
   */
  private void setConfig(String key, String value) {
    ContentValues values = new ContentValues();
    values.put(AccountDb.KEY_COLUMN, key);
    values.put(AccountDb.VALUE_COLUMN, value);
    int updated = mDatabase.update(AccountDb.TABLE2_NAME, values,
			  AccountDb.KEY_COLUMN + " = " + DatabaseUtils.sqlEscapeString(key), null);
    if (updated == 0) {
      mDatabase.insert(AccountDb.TABLE2_NAME, null, values);
    }
  }
  
  /**
   * Encrypts all secrets found in database.
   *
   * 
   */
  private void encryptDatabase() {
    if (passwordHash!=null) {
      Cursor cursor=getNames();
     
      try {
        if (!cursorIsEmpty(cursor))
        {
          int nameCount = cursor.getCount();
          int emailIndex = cursor.getColumnIndex(AccountDb.EMAIL_COLUMN);
          int secretIndex = cursor.getColumnIndex(AccountDb.SECRET_COLUMN);
      
          for (int i = 0; i < nameCount; ++i) {
            cursor.moveToPosition(i);
            String email = cursor.getString(emailIndex);
            String secret = cursor.getString(secretIndex);
            ContentValues values = new ContentValues();
            values.put(EMAIL_COLUMN, email);
            values.put(SECRET_COLUMN,encryptString(secret));
            mDatabase.update(TABLE_NAME, values, whereClause(email), null);
          }
        }
  
      } finally {
        tryCloseCursor(cursor);
      }
      setConfig(AccountDb.CONFIG_ENCRYPTED,AccountDb.CONFIG_ENCRYPTED_TRUE);
    }
    else {
      //FIXME: Raise an exception or log an error?
    }
  }


  /**
   * Removes encryption from all secrets found in database.
   *
   * 
   */
  private void decryptDatabase() {
    if (passwordHash!=null) {
      Cursor cursor=getNames();
      
      try {
        if (!cursorIsEmpty(cursor))
        {
          int nameCount = cursor.getCount();
          int emailIndex = cursor.getColumnIndex(AccountDb.EMAIL_COLUMN);
          int secretIndex = cursor.getColumnIndex(AccountDb.SECRET_COLUMN);
      
          for (int i = 0; i < nameCount; ++i) {
            cursor.moveToPosition(i);
            String email = cursor.getString(emailIndex);
            String secret = decryptString(cursor.getString(secretIndex));
            ContentValues values = new ContentValues();
            values.put(EMAIL_COLUMN, email);
            values.put(SECRET_COLUMN,secret);
            mDatabase.update(TABLE_NAME, values, whereClause(email), null);
          }
        }
  
      } finally {
        tryCloseCursor(cursor);
      }
      setConfig(AccountDb.CONFIG_ENCRYPTED,AccountDb.CONFIG_ENCRYPTED_FALSE);
    }    
    else {
      //FIXME: Raise an exception or log an error?
    }
  }
  
  /**
   * Generates a SHA-256 hash. It with be interact HASH_INTERATIONS times.
   */
  private byte[] getHash(byte[] input) {
    MessageDigest digest;
    try {
        digest = MessageDigest.getInstance("SHA-256");
        digest.reset();
        byte[] h=digest.digest(input);
        for (byte i=0;i<HASH_INTERATIONS-1;i++)
        {   
            byte [] h2= new byte[h.length+1];
            System.arraycopy(h,0,h2,0,h.length);
            h2[h.length]=i;
            h=digest.digest(h2);
        }
        
        return h;
    } catch (NoSuchAlgorithmException error) {
        Log.e(LOCAL_TAG, error.getMessage());
    }
    byte[] vector = new byte[32];
    Arrays.fill(vector, (byte)0);
    return vector;
    
  }
  /**
   * Generates the hash used to encrypt secrets.
   * Password will be concatenated to android_id and a SHA-256 digest will be generated.
   *
   * @return password's hash
   */
  private byte[] getPasswordHash(String password) {
	String id2 = Secure.getString(myContext.getContentResolver(), Secure.ANDROID_ID); 
	String finalString = password+id2;
	try {
		return getHash(finalString.getBytes("UTF-8"));
	} catch (UnsupportedEncodingException error) {
		Log.e(LOCAL_TAG, error.getMessage());
	}
    byte [] result = new byte[32];
    Arrays.fill(result, (byte)0);
	return result;
  }
  
  /**
   * Encrypts or decrypts an string.
   * 
   * @return encrypted/decrypted string (base32 encoded)
   */
  private String cipherString(String inStr, int mode) throws DecodingException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, InvalidAlgorithmParameterException, BadPaddingException {
  
    if (passwordHash==null)
      return inStr;
    
	Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
	SecretKeySpec key = new SecretKeySpec(passwordHash, "AES");
	String result=""; 

	byte[] bytes=decodeKey(inStr);
	byte[] iv=new byte[c.getBlockSize()];
	Arrays.fill(iv, (byte)0);
	IvParameterSpec ivSpec = new IvParameterSpec(iv);
	c.init(mode, key, ivSpec);
	result=encodeKey(c.doFinal(bytes));
	
	return result;	
  }
  
  /**
   * Decrypts an string.
   * 
   * @return decrypted string (base32 encoded)
   */
  private String decryptString(String inStr) { 
	String outStr=inStr;
    try {
      outStr = cipherString(inStr, Cipher.DECRYPT_MODE);
    }
    catch (Exception e) {
      Log.e(LOCAL_TAG, e.getMessage());
	}
	return outStr;
  }

  /**
   * Encrypts an string.
   * 
   * @return encrypted string (base32 encoded)
   */
  private String encryptString(String inStr) {
  	String outStr=inStr;
    try {
      outStr = cipherString(inStr, Cipher.ENCRYPT_MODE);
    }
    catch (Exception e) {
      Log.e(LOCAL_TAG, e.getMessage());
	}
	return outStr;
  }
  
  public boolean isPasswordSet() {
    return passwordHash!=null;
  }
  
  /**
   * Sets password used to encrypt/decrypt secrets.
   */
  public boolean setPassword(String password) {
    try {
      byte[] currentPasswordHash = decodeKey(getConfig(CONFIG_PASSWORD_HASH));
      if (password!=null) {
        byte[] newPasswordHash = getPasswordHash(password);
        if (currentPasswordHash.length==0 || Arrays.equals(getHash(newPasswordHash), currentPasswordHash)) {
          passwordHash=newPasswordHash;
          return true;
        }
      }
      else
        passwordHash=null;
    } catch (DecodingException error) {
        Log.e(LOCAL_TAG, error.getMessage());
    }
    return false;
  }
  
  /**
   * Resets password used to encrypt/decrypt secrets.
   */
  public void resetPassword() {
    passwordHash=null;
  }

  /**
   * Returns if database is empty.
   */
  public boolean isEmpty() {
    Cursor cursor=getNames();
    try {
      if (cursorIsEmpty(cursor))
        return true;
      return false;
      } finally {
        tryCloseCursor(cursor);
      }
  }
  
  /**
   * Saves hashed password on configuration file. 
   */
  private boolean savePasswordHash()  {
      if (passwordHash==null)
        return false;
      try {
        setConfig(CONFIG_PASSWORD_HASH, encodeKey(getHash(passwordHash)));
      } catch (DecodingException error) {
        Log.e(LOCAL_TAG, error.getMessage());
      }
      return true;
  }
  
  /**
   * Deletes hashed password from configuration file. 
   */
  private void deletePasswordHash() {
      setConfig(CONFIG_PASSWORD_HASH, "");
  }
  
  /**
   * Saves given password and encrypt database. 
   */
  public void savePassword(String password) {
    setPassword(password);
    // Encrypts using new password
    encryptDatabase();
    savePasswordHash(); 
  }
   
  /**
   * Changes the password and re-encrypt database. 
   */
  public boolean changePassword(String currentPassword, String newPassword){
    byte[] currentPasswordHash;

    try {
      currentPasswordHash = decodeKey(getConfig(CONFIG_PASSWORD_HASH));
      if (!(Arrays.equals(passwordHash,getPasswordHash(currentPassword)) &&
          Arrays.equals(getHash(passwordHash), currentPasswordHash)))
        return false;

      
      byte[] newPasswordHash=getPasswordHash(newPassword);
      //Only acts if new password is different from current one.
      if (!Arrays.equals(passwordHash,newPasswordHash))
      {
        // Decrypts using current password
        decryptDatabase();
        resetPassword();
        deletePasswordHash();
        if (newPassword!=null) {
          setPassword(newPassword);
          savePasswordHash();
          // Encrypts using new password
          encryptDatabase();
        }
      }
    } catch (DecodingException error) {
      Log.e(LOCAL_TAG, error.getMessage());
    }
    return true;
  }
}
